#!/usr/bin/php -q
<?php
// Try to load Net_SmartIRC and error and die if we can't
if ( ! require_once( 'Net/SmartIRC.php' ) ) {
	die( 'wp-trac-bot requires the Net_SmartIRC pear package. Please install this package and restart the bot.' );
}

// Try to load the config file and error and die if we can't
if ( ! require_once( 'trac-bot-config.php' ) ) {
	die( 'Could not include the trac-bot-config.php config file. Please create this file from trac-bot-config-sample.php and restart the bot.' );
}

/* Version of this bot */
define( 'VERSION', '1.1.1' );

class trac {

	function __construct() {
		global $trac_hosts;
		$this->base_trac_url = $trac_hosts;
	}

	function http( $url, $method = 'get' ) {
		if ( strtolower( $method ) == 'head' )
			$url = preg_replace( '/#.*$/', '', $url );

		$session = curl_init( $url );

		curl_setopt( $session, CURLOPT_HEADER, false );
		curl_setopt( $session, CURLOPT_RETURNTRANSFER, true );
		curl_setopt( $session, CURLOPT_FOLLOWLOCATION, true );
		curl_setopt( $session, CURLOPT_MAXREDIRS, 5 );
		curl_setopt( $session, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 );
		curl_setopt( $session, CURLOPT_SSL_VERIFYHOST, false );
		curl_setopt( $session, CURLOPT_SSL_VERIFYPEER, false );

		if ( strtolower( $method ) == 'head' )
			curl_setopt( $session, CURLOPT_NOBODY, true );

		$response['body'] = curl_exec( $session );
		$response['code'] = curl_getinfo( $session, CURLINFO_HTTP_CODE );

		curl_close( $session );

		return $response;
	}

	function ticket( &$irc, &$data ) {
		global $irc_channels;
		$pre_pattern = ( $irc_channels[$data->channel] === true ) ? '(^' . IRC_NICK . '.+\ )' : '(\ |^)';
		$pattern = "/($pre_pattern#(WP)?|\/ticket\/)(\d{4,6})(\b|$)/i";
		preg_match_all( $pattern, $data->message, $tickets, PREG_SET_ORDER );
		if ( is_array( $tickets ) && ! empty( $tickets ) ) {
			// Build an array of only the ticket numbers, an array that does not include other matched text
			foreach ( $tickets as $ticket ) {
				$ticket_numbers[] = $ticket[4];
			}

			// Make sure we don't have duplicate ticket numbers, we don't need to echo the same thing multiple times
			$ticket_numbers = array_unique( $ticket_numbers );
			foreach ( $ticket_numbers as $ticket_number ) {
				$base_trac_url = isset( $this->base_trac_url[$data->channel] ) ? $this->base_trac_url[$data->channel] : $this->base_trac_url['fallback'];

				// #bbpress seems to have a bot that also chats about bbPress plugins, so lets ignore those, also ignore test cases
				if ( preg_match( '/(bbPress\ Plugins|wordpress-tests:)/i', $data->message ) )
					return;
				else if ( preg_match( '/BackPress:/', $data->message ) && $data->channel != '#backpress' )
					return;
				else
					$url = $base_trac_url . '/ticket/' . $ticket_number . '?format=csv';

				$response = $this->http( $url );
				// Make sure that the ticket exists and we get data back before trying to parse it
				if ( $response['code'] == 200 ) {
					// The CSV returned from trac returns a title row and the data, the str_getcsv function only
					// operates on a single row, so split the CSV at the line break and operate on each row individually
					list( $keys_string, $values_string ) = preg_split( '/\n/', $response['body'], 2 );
					$keys_array = str_getcsv( $keys_string, ',', '"', '"' );
					$values_array = str_getcsv( $values_string, ',', '"', '"' );

					// Merge the ticket data into a single array, where the keys are the field titles
					$ticket_data = array();
					foreach ( $keys_array as $intkey => $strkey )
						$ticket_data[$strkey] = $values_array[$intkey];

					unset( $keys_array, $values_array );

					// Build the output string
					if ( empty( $ticket_data['owner'] ) )
						$ticket_data['owner'] = '(no owner)';

					$output = $data->nick . ': ' . $base_trac_url . '/ticket/' . $ticket_number . ' ';

					$severity = empty( $ticket_data['severity'] ) ? '' : $ticket_data['severity'];
					if ( 'normal' == $severity )
						$severity = '';
					$priority = 'normal' == $ticket_data['priority'] ? '' : $ticket_data['priority'];
					if ( $severity || $priority )
						$output .= $severity . ( $severity && $priority ? '/' : '' ) . $priority . ', ';

					if ( empty( $ticket_data['milestone'] ) )
						$output .= 'Unassigned';
					else
						$output .= $ticket_data['milestone'];

					$output .= ', ' . $ticket_data['reporter'];
					$output .= '->' . $ticket_data['owner'];
					$output .= ', ' . $ticket_data['status'];
					$output .= ', ' . $ticket_data['summary'];
				} else {
					$output = $data->nick . ": Ticket does not exist";
				}
				unset( $response );
				// echo the above output string
				$irc->message( SMARTIRC_TYPE_CHANNEL, $data->channel, $output );
			}
		}
	}

	function changeset( &$irc, &$data ) {
		global $irc_channels;
		$pre_pattern = ( $irc_channels[$data->channel] === true ) ? '(^' . IRC_NICK . '.+\ )' : '(\ |^|\b)';
		// The ^B characters specify bold, the CIA bots tend to put the revisions in bold
		$pattern = "/{$pre_pattern}([r\[])?(\d{3,6}|head)\]??(\b|$)/i";
		preg_match_all( $pattern, $data->message, $revisions, PREG_SET_ORDER );

		if ( is_array( $revisions ) && ! empty( $revisions ) ) {
			// Build an array of only revision numbers, an array without any of the other matched text
			foreach ( $revisions as $revision ) {
				$revision_numbers[] = $revision[3];
			}

			// Make sure we don't have duplicate revisions numbers, we don't need to echo the same thing multiple times
			$revision_numbers = array_unique( $revision_numbers );
			foreach ( $revision_numbers as $revision_number ) {
				if ( is_string( $revision_number ) && strtolower( $revision_number ) == 'head' )
					$is_head = true;
				else
					$is_head = false;

				$base_trac_url = isset( $this->base_trac_url[$data->channel] ) ? $this->base_trac_url[$data->channel] : $this->base_trac_url['fallback'];

				// #bbpress seems to have a bot that also chats about bbPress plugins, so lets ignore those, also ignore test cases
				if ( preg_match( '/(bbPress\ Plugins|wordpress-tests:)/i', $data->message ) )
					return;
				else if ( preg_match( '/BackPress:/', $data->message ) && $data->channel != '#backpress' )
					return;
				else if ( $is_head )
					$url = $base_trac_url . '/timeline?changeset=on&max=1&format=rss';
				else
					$url = $base_trac_url . '/changeset/' . $revision_number;

				// Let's make sure the changeset exists before echoing a url
				$response = $this->http( $url, ( $is_head ) ? 'get' : 'head' );	

				if ( $response['code'] == 200 ) {
					if ( ! empty( $response['body'] ) ) {
						preg_match_all( '%<link>([^<]+)</link>%', $response['body'], $links, PREG_SET_ORDER );
						$url = $links[2][1];
					}
					$output = $data->nick . ": $url";
				} else {
					$output = $data->nick . ": Changeset does not exist";
				}

				// echo the url of the changeset
				$irc->message( SMARTIRC_TYPE_CHANNEL, $data->channel, $output );
			}
		}
	}
}

class admin {
	function is_admin( $data ) {
		global $bot_admins;
		if ( isset( $bot_admins[$data->nick] ) && $bot_admins[$data->nick] == $data->host )
			return true;
		else
			return false;
	}

	function join( $irc, $data ) {
		global $irc_channels;
		if ( ! $this->is_admin( $data ) )
			return;

		list( $command, $query ) = explode( ' ', $data->message );

		if ( strstr( $query, ':' ) ) {
			list( $channel, $require_nick ) = explode( ':', $query );
			$require_nick = ( $require_nick == 'true' ) ? true : false;
		} else {
			$channel = $query;
			$require_nick = true;
		}

		if ( ! $irc->isJoined( $channel ) ) {
			$irc->message( SMARTIRC_TYPE_QUERY, $data->nick, "Joining $channel" );
			$irc->join( array( $channel ) );
			$irc_channels[$channel] = $require_nick;
		} else {
			$irc->message( SMARTIRC_TYPE_QUERY, $data->nick, "Already joined to $channel" );
		}
	}

	function part( $irc, $data ) {
		global $irc_channels;
		if ( ! $this->is_admin( $data ) )
			return;

		list( $command, $query ) = explode( ' ', $data->message );

		if ( $irc->isJoined( $query ) ) {
			$irc->message( SMARTIRC_TYPE_QUERY, $data->nick, "Parting $query" );
			$irc->part( array( $query ) );
			unset( $irc_channels[$query] );
		} else {
			$irc->message( SMARTIRC_TYPE_QUERY, $data->nick, "Not joined to $query" );
		}
	}

	function nick( $irc, $data ) {
		if ( ! $this->is_admin( $data ) )
			return;

		list( $command, $query ) = explode( ' ', $data->message );
		
		if ( ! $irc->isMe( $query ) ) {
			$irc->message( SMARTIRC_TYPE_QUERY, $data->nick, "Changing nick to $query" );
			$irc->changeNick( $query );
		} else {
			$irc->message( SMARTIRC_TYPE_QUERY, $data->nick, "Nick is already $query" );
		}
	}

	function quit( $irc, $data ) {
		if ( ! $this->is_admin( $data ) )
			return;

		$irc->message( SMARTIRC_TYPE_QUERY, $data->nick, "Quitting" );
		$irc->quit( 'Quit Requested', SMARTIRC_CRITICAL );
	}

	function ping( $irc, $data ) {
		$irc->message( SMARTIRC_TYPE_QUERY, $data->nick, "pong" );
	}

	function where( $irc, $data ) {
		$irc->message( SMARTIRC_TYPE_QUERY, $data->nick, 'Channels: ' . implode(',', array_keys($irc->_channels)));
	}

}

$trac = &new trac();
$admin = &new admin();
$irc = &new Net_SmartIRC();
$irc->setDebug(SMARTIRC_DEBUG_ALL);
$irc->setAutoReconnect( true );
$irc->setAutoRetry( true );
$irc->setUseSockets( true );
$irc->setCtcpVersion( BOT_DESCRIPTION . ' v' . VERSION );
$irc->setChannelSyncing( true );
$irc->registerActionhandler( SMARTIRC_TYPE_CHANNEL, '/(\ |^|\b)([r\[])?(\d{3,6}|head)\]??(\b|$)/i', $trac, 'changeset' );
$irc->registerActionhandler( SMARTIRC_TYPE_CHANNEL, '/((\ |^)#(WP)?|\/ticket\/)\d{4,6}(\b|$)/i', $trac, 'ticket' );
$irc->registerActionhandler( SMARTIRC_TYPE_QUERY, '/^part\ #.+/i', $admin, 'part' );
$irc->registerActionhandler( SMARTIRC_TYPE_QUERY, '/^join\ #.+/i', $admin, 'join' );
$irc->registerActionhandler( SMARTIRC_TYPE_QUERY, '/^quit$/i', $admin, 'quit' );
$irc->registerActionhandler( SMARTIRC_TYPE_QUERY, '/^nick\ .+/i', $admin, 'nick' );
$irc->registerActionhandler( SMARTIRC_TYPE_QUERY, '/^ping$/', $admin, 'ping' );
$irc->registerActionhandler( SMARTIRC_TYPE_QUERY, '/^where$/', $admin, 'where' );
$irc->connect( IRC_HOST, IRC_PORT );
$irc->login( IRC_NICK, IRC_REALNAME, IRC_USERMODE, IRC_USERNAME, IRC_PASSWORD );
sleep( 5 ); // Sleep before joining channels
$irc->join( array_keys( $irc_channels ) );
$irc->listen();
$irc->disconnect();
